use crate::util::{get_json_i64, get_json_str, get_json_str_with_default};
use crate::utils::{calc_d_access_token, create_easy};
use crate::{try_curl, try_curl_form, try_curl_multi};
use alloc::borrow::ToOwned;
use alloc::boxed::Box;
use alloc::format;
use alloc::str;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::time::Duration;
use curl::easy::Easy;
use curl::multi::Multi;
use embed_std::time::now;
use embed_std::{cstr_ptr, debugln, errorln, infoln, libc, Error};

mod capi;

const RETRY: u8 = 2;
const EXPIRE: u32 = 3600;
const TIMEOUT: u32 = 120;
const DEFAULT_APP_ID: &str = "eIoTLink";
const DEFAULT_LANG: &str = "zh-cn";

pub struct UploadContext {
    base_url: String,
    device_uid: String,
    secret: String,
    expire: u32,
    upload_args: Option<UploadArgs>,
    timeout: u32,
}

#[derive(Debug, Default)]
pub(crate) struct UploadArgs {
    pub access_key: String,
    pub security_token: String,
    pub policy: String,
    pub signature: String,
    pub prefix: String,
    pub url: String,
    pub authorization: String,
    pub expire: i64,
    pub oss_type: i32,
}

pub struct UploadResponse {
    pub url: String,
}

impl UploadContext {
    pub fn new(base_url: &str, device_uid: &str, secret: &str) -> Box<Self> {
        Box::new(Self {
            base_url: base_url.to_owned(),
            device_uid: device_uid.to_owned(),
            secret: secret.to_owned(),
            expire: EXPIRE,
            upload_args: None,
            timeout: TIMEOUT,
        })
    }

    pub fn set_timeout(&mut self, timeout: u32) {
        if timeout > 0 {
            self.timeout = timeout;
        }
    }

    pub(crate) fn parse_token(data: &[u8]) -> embed_std::Result<UploadArgs> {
        let json = str::from_utf8(data).map_err(|e| Error::Other(e.to_string()))?;
        let root: cjson::Json =
            cjson::Json::parse(json).map_err(|e| Error::Other(format!("{:?}", e)))?;
        if root.is_object() {
            let rc = root
                .get(cstr_ptr!("rc"))
                .ok_or(Error::NoJsonField("rc".to_owned()))?;
            if !rc.is_number() {
                return Err(Error::InvalidJsonField("rc".to_owned()));
            }
            if rc.as_i64() != Some(0) {
                let rd = if let Some(rd) = root.get(cstr_ptr!("rd")) {
                    rd.as_str().unwrap_or("failed").to_owned()
                } else {
                    "failed".to_owned()
                };
                return Err(Error::Io(rc.as_i64().unwrap() as i32, rd));
            }
            let data = root
                .get(cstr_ptr!("data"))
                .ok_or(Error::NoJsonField("data".to_owned()))?;
            if !data.is_object() {
                return Err(Error::InvalidJsonField("data".to_owned()));
            }

            let args = UploadArgs {
                access_key: get_json_str_with_default(&data, "access_key", "")?,
                security_token: get_json_str_with_default(&data, "security_token", "")?,
                policy: get_json_str(&data, "policy")?,
                signature: get_json_str(&data, "signature")?,
                prefix: get_json_str(&data, "prefix")?,
                url: get_json_str(&data, "url")?,
                authorization: get_json_str(&data, "authorization")?,
                expire: get_json_i64(&data, "expire")?,
                oss_type: get_json_i64(&data, "oss_type")? as i32,
            };
            return Ok(args);
        }
        Err(Error::Other(format!("invalid json:{}", json)))
    }

    pub(crate) fn get_token(&mut self) -> embed_std::Result<UploadArgs> {
        let mut buffer: Vec<u8> = Vec::new();
        {
            let url = format!(
                "{}/v1/storage/devices/{}/token",
                self.base_url, self.device_uid
            );
            let mut curl = create_easy(url.as_str())?;
            infoln!("get token url {}", url);
            let mut headers = curl::easy::List::new();
            try_curl!(headers.append("Content-Type: Application/json;charset=UTF-8"))?;
            let lang = format!("X-Lang: {}", DEFAULT_LANG);
            try_curl!(headers.append(lang.as_str()))?;
            let app_id = format!("X-App: {}", DEFAULT_APP_ID);
            try_curl!(headers.append(app_id.as_str()))?;
            try_curl!(headers.append(
                format!(
                    "D-AccessToken: {}",
                    calc_d_access_token(&self.device_uid, &self.secret)
                )
                .as_str()
            ))?;
            try_curl!(curl.http_headers(headers))?;
            try_curl!(curl.timeout(core::time::Duration::from_secs(10)))?;
            try_curl!(
                curl.post_fields_copy(format!("{{ \"expire\" : {} }}", self.expire).as_bytes())
            )?;
            try_curl!(curl.post(true))?;
            let mut handle = curl.transfer();
            try_curl!(handle.write_function(|data| {
                buffer.extend(data);
                Ok(data.len())
            }))?;
            try_curl!(handle.perform())?;
        }
        infoln!("{}", String::from_utf8_lossy(buffer.as_slice()));
        Self::parse_token(buffer.as_slice())
    }

    pub fn upload(&mut self, key: &str, filename: &str) -> embed_std::Result<UploadResponse> {
        let mut last_err: embed_std::Result<UploadResponse> =
            Err(Error::Other("unknown".to_owned()));
        for _i in 0..RETRY {
            let cur = now().as_secs() as i64;
            if self.upload_args.is_none()
                || cur > self.upload_args.as_ref().unwrap().expire * 8 / 10
            {
                self.upload_args = None;
                match self.get_token() {
                    Ok(args) => {
                        self.upload_args = Some(args);
                    }
                    Err(err) => {
                        last_err = Err(err);
                        continue;
                    }
                }
            }

            let ret = match self.upload_args.as_ref().unwrap().oss_type {
                0 => self.upload_aliyun(key, filename),
                1 => self.upload_aws(key, filename),
                n => {
                    last_err = Err(Error::Other(format!("unknown oss_type {}", n)));
                    continue;
                }
            };
            match ret {
                Ok(r) => {
                    return Ok(r);
                }
                Err(err) => {
                    errorln!(
                        "upload device_uid {} key {} filename {} failed: {}",
                        self.device_uid,
                        key,
                        filename,
                        err
                    );
                    last_err = Err(err);
                    continue;
                }
            }
        }
        last_err
    }

    fn upload_aws(&mut self, key: &str, filename: &str) -> embed_std::Result<UploadResponse> {
        let args = self.upload_args.as_ref().unwrap();
        let mut client = create_easy(args.url.as_str())?;
        infoln!(
            "upload to aws: url {} prefix {} filename {}",
            args.url,
            key,
            filename
        );
        let full_key = format!("{}{}", args.prefix, key);
        infoln!("full key {}", full_key);
        let mut form = curl::easy::Form::new();
        try_curl_form!(form.part("key").contents(full_key.as_bytes()).add())?;
        try_curl_form!(form.part("policy").contents(args.policy.as_bytes()).add())?;
        try_curl_form!(form
            .part("x-amz-Signature")
            .contents(args.signature.as_bytes())
            .add())?;
        try_curl_form!(form
            .part("x-amz-credential")
            .contents(args.authorization.as_bytes())
            .add())?;
        try_curl_form!(form
            .part("success_action_status")
            .contents(200.to_string().as_bytes())
            .add())?;
        try_curl_form!(form
            .part("X-Amz-Algorithm")
            .contents("AWS4-HMAC-SHA256".as_bytes())
            .add())?;
        let cur_time = self.get_utc_time();
        try_curl_form!(form.part("X-Amz-Date").contents(cur_time.as_bytes()).add())?;
        try_curl_form!(form.part("file").file(filename).add())?;
        try_curl!(client.httppost(form))?;
        self.send(client, key, filename)?;
        Ok(UploadResponse {
            url: format!("{}/{}", args.url, full_key),
        })
    }

    fn get_utc_time(&self) -> String {
        unsafe {
            let t = libc::time(core::ptr::null_mut());
            let mut tm: libc::tm = core::mem::zeroed();
            libc::gmtime_r(&t, &mut tm);
            format!(
                "{:04}{:02}{:02}T{:02}{:02}{:02}Z",
                tm.tm_year + 1900,
                tm.tm_mon + 1,
                tm.tm_mday,
                tm.tm_hour,
                tm.tm_min,
                tm.tm_sec
            )
        }
    }

    fn upload_aliyun(&mut self, key: &str, filename: &str) -> embed_std::Result<UploadResponse> {
        let args = self.upload_args.as_ref().unwrap();
        let mut client = create_easy(args.url.as_str())?;
        infoln!(
            "upload to aliyun: url {} prefix {} filename {}",
            args.url,
            key,
            filename
        );
        let full_key = format!("{}{}", args.prefix, key);
        infoln!("full key {}", full_key);
        let mut headers = curl::easy::List::new();
        try_curl!(headers.append(format!("Authorization: {}", args.authorization).as_str()))?;
        try_curl!(client.http_headers(headers))?;
        let mut form = curl::easy::Form::new();
        try_curl_form!(form.part("key").contents(full_key.as_bytes()).add())?;
        try_curl_form!(form.part("policy").contents(args.policy.as_bytes()).add())?;
        try_curl_form!(form
            .part("OSSAccessKeyId")
            .contents(args.access_key.as_bytes())
            .add())?;
        try_curl_form!(form
            .part("success_action_status")
            .contents(200.to_string().as_bytes())
            .add())?;
        try_curl_form!(form
            .part("signature")
            .contents(args.signature.as_bytes())
            .add())?;
        try_curl_form!(form
            .part("x-oss-security-token")
            .contents(args.security_token.as_bytes())
            .add())?;
        try_curl_form!(form.part("file").file(filename).add())?;
        try_curl!(client.httppost(form))?;
        self.send(client, key, filename)?;
        Ok(UploadResponse {
            url: format!("{}/{}", args.url, full_key),
        })
    }

    fn send(&self, client: Easy, key: &str, filename: &str) -> embed_std::Result<()> {
        let multi = Multi::new();
        let mut handler = try_curl_multi!(multi.add(client))?;
        let st = now().as_secs() as i64;
        loop {
            match multi.perform() {
                Ok(ret) => {
                    debugln!("multi perform return {}", ret);
                    if ret == 0 {
                        match handler.response_code() {
                            Ok(code) => {
                                if code >= 100 && code < 300 {
                                    infoln!("resp: {}", code);
                                    break;
                                } else {
                                    return Err(Error::Other(format!("error {}", code)));
                                }
                            }
                            Err(e) => {
                                return Err(Error::Other(e.to_string()));
                            }
                        }
                    }
                    try_curl_multi!(multi.wait(&mut [], Duration::from_secs(1)))?;
                    if now().as_secs() as i64 > st + self.timeout as i64 {
                        errorln!("upload key {} filename {} timeout", key, filename);
                        return Err(Error::Timeout);
                    }
                }
                Err(err) => {
                    errorln!("multi post failed: {}", err);
                    return Err(Error::Other(err.to_string()));
                }
            }
        }
        Ok(())
    }
}
